[org 0x900]

%include "boot.inc"

[bits 16]
section .text
start:
    mov ax,ds
    mov es,ax
;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------
mem_e820:
    ;magic_break
    ;xchg bx,bx
    xor ebx,ebx
    mov edx,0x534d4150
    mov di,ards_buf
.mem_e820_loop:
    mov eax,0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号
    mov ecx,20         ;ARDS地址范围描述符结构大小是20字节
    int 0x15
    jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生，尝试0xe801子功能
    ;magic_break
    ;xchg bx,bx
    add di,cx                   ;使di增加20字节指向缓冲区中新的ARDS结构位置
    inc word [ards_nr]	      ;记录ARDS数量
    cmp ebx, 0		      ;若ebx为0且cf不为1,这说明ards全部返回，当前已是最后一个
    jnz .mem_e820_loop
    ;在所有ards结构中，找出(base_add_low + length_low)的最大值，即内存的容量
    mov cx, [ards_nr]	      ;遍历每一个ARDS结构体,循环次数是ARDS的数量
    mov ebx, ards_buf
    xor edx, edx		      ;edx为最大的内存容量,在此先清0
.find_max_mem_area:           ;无须判断type是否为1,最大的内存块一定是可被使用
    mov eax,[ebx]   ;base_addr_low
    add eax,[ebx+8] ;length_low
    add ebx,20      ;next ards struct
    cmp edx,eax
    jge .next_ards
    mov edx,eax
.next_ards:
    loop .find_max_mem_area
    jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
.e820_failed_so_try_e801:
    ;magic_break
    ;xchg bx,bx
    mov ax,0xe801
    int 0x15
    jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法
    ;1.先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
    mov cx,0x400	     ;cx和ax值一样,cx用做乘数
    mul cx
    and eax,0x0000FFFF
    or edx,eax
    add edx, 0x100000 ;ax只是15MB,故要加1MB
    mov esi,edx	     ;先把低15MB的内存容量存入esi寄存器备份
    ;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
    xor eax,eax
    mov ax,bx
    mov ecx, 0x10000	;0x10000十进制为64KB
    mul ecx		;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
    add esi,eax		;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
    mov edx,esi		;edx为总内存大小
    jmp .mem_get_ok
;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88:
   ;magic_break
   ;xchg bx,bx
   ;int 15后，ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
   ;16位乘法，被乘数是ax,积为32位.积的高16位在dx中，积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16位
   or edx, eax	     ;把积的低16位组合到edx,为32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
.error_hlt:		      ;出错则挂起
   hlt

.mem_get_ok:
   mov [total_mem_bytes], edx	 ;将内存换为byte单位后存入total_mem_bytes处

   ;open A20
   in al,0x92
   or al,00000010b
   out 0x92,al
   ;load gdt
   lgdt [gdt_ptr]
   ;set cr0 pe
   mov eax,cr0
   or eax,00000001b
   mov cr0,eax

   jmp dword SELECTOR_CODE:p_mode_start

[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov ax, SELECTOR_VIDEO
   mov gs, ax
   mov byte [gs:160],'P'
   ;magic_break
   ;xchg bx,bx
   mov eax,KERNEL_START_SECTOR ;kernel.bin所在的扇区号
   mov ebx,KERNEL_BIN_BASE_ADDR ;从磁盘读出后，写入到ebx指定的地址
   mov ecx, 200			       ; 读入的扇区数
   ;xchg bx,bx
   call rd_disk_m_32
   ;创建页目录及页表并初始化页内存位图
   call setup_page
   ;xchg bx,bx
   sgdt [gdt_ptr] ;read gdt to special memory location

   mov ebx,[gdt_ptr+2] ;get gdt base addr
   or dword [ebx+0x18+4], 0xc0000000  ;set video addr high 4

   add dword [gdt_ptr + 2], 0xc0000000
   add esp, 0xc0000000

   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:162], 'i'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:164], 'r'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:166], 't'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:168], 'u'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:170], 'a'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:172], 'l'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp dword SELECTOR_CODE:enter_kernel

enter_kernel:
   mov byte [gs:320], 'k'     ;视频段段基址已经被更新
   mov byte [gs:322], 'e'     ;视频段段基址已经被更新
   mov byte [gs:324], 'r'     ;视频段段基址已经被更新
   mov byte [gs:326], 'n'     ;视频段段基址已经被更新
   mov byte [gs:328], 'e'     ;视频段段基址已经被更新
   mov byte [gs:330], 'l'     ;视频段段基址已经被更新

   mov byte [gs:480], 'w'     ;视频段段基址已经被更新
   mov byte [gs:482], 'h'     ;视频段段基址已经被更新
   mov byte [gs:484], 'i'     ;视频段段基址已经被更新
   mov byte [gs:486], 'l'     ;视频段段基址已经被更新
   mov byte [gs:488], 'e'     ;视频段段基址已经被更新
   mov byte [gs:490], '('     ;视频段段基址已经被更新
   mov byte [gs:492], '1'     ;视频段段基址已经被更新
   mov byte [gs:494], ')'     ;视频段段基址已经被更新
   mov byte [gs:496], ';'     ;视频段段基址已经被更新
   call kernel_init
   ;xchg bx,bx
   mov esp, 0xc009f000
   xchg bx,bx
   jmp KERNEL_ENTRY_POINT ;跳转至0xc0001500跳执行C程序

kernel_init:
   xor eax, eax
   xor ebx, ebx		;ebx记录程序头表地址
   xor ecx, ecx		;cx记录程序头表中的program header数量
   xor edx, edx		;dx 记录program header尺寸,即e_phentsize

   mov dx,[KERNEL_BIN_BASE_ADDR + 42]	  ;偏移文件42字节处的属性是e_phentsize,表示program header大小
   mov ebx,[KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1个program header在文件中的偏移量
   add ebx, KERNEL_BIN_BASE_ADDR
   mov cx, [KERNEL_BIN_BASE_ADDR + 44]    ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header

.each_segment:
   cmp byte [ebx + 0],PT_NULL		  ; 若p_type等于 PT_NULL,说明此program header未使用
   je .PTNULL

   ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)
   push dword [ebx + 16]		  ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size
   mov eax, [ebx + 4]			  ; 距程序头偏移量为4字节的位置是p_offset
   add eax, KERNEL_BIN_BASE_ADDR  ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
   push eax				          ; 压入函数memcpy的第二个参数:源地址
   push dword [ebx + 8]			  ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr，这就是目的地址
   call mem_cpy				      ; 调用mem_cpy完成段复制
   add esp,12				      ; 清理栈中压入的三个参数

.PTNULL:
   add ebx, edx				  ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header
   loop .each_segment
   ret

;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:
   cld
   push ebp
   mov ebp, esp
   push ecx		   ; rep指令用到了ecx，但ecx对于外层段的循环还有用，故先入栈备份
   mov edi, [ebp + 8]	   ; dst
   mov esi, [ebp + 12]	   ; src
   mov ecx, [ebp + 16]	   ; size
   rep movsb		   ; 逐字节拷贝

   ;恢复环境
   pop ecx
   pop ebp
   ret

rd_disk_m_32:
     mov esi,eax	   ;备份eax
     mov di,cx		   ;备份扇区数到di
;第1步：设置要读取的扇区数
     mov dx,0x1f2
     mov al,cl
     out dx,al         ;读取的扇区数
;第2步：将LBA地址存入0x1f3 ~ 0x1f6
     ;LBA地址7~0位写入端口0x1f3
     mov eax,esi	   ;恢复ax
     mov dx,0x1f3
     out dx,al
      ;LBA地址15~8位写入端口0x1f4
     mov cl,8
     shr eax,cl
     mov dx,0x1f4
     out dx,al

     ;LBA地址23~16位写入端口0x1f5
     shr eax,cl
     mov dx,0x1f5
     out dx,al

     shr eax,cl
     and al,0x0f	   ;lba第24~27位
     or al,0xe0	       ; 设置7～4位为1110,表示lba模式
     mov dx,0x1f6
     out dx,al

;第3步：向0x1f7端口写入读命令，0x20
     mov dx,0x1f7
     mov al,0x20
     out dx,al
;第4步：检测硬盘状态
.not_ready:		   ;测试0x1f7端口(status寄存器)的的BSY位
     nop
     in al,dx
     and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
     cmp al,0x08
     jnz .not_ready	   ;若未准备好,继续等

;第5步：从0x1f0端口读数据
     mov ax, di
     mov dx, 256
     mul dx
     mov cx, ax
     mov dx, 0x1f0
.go_on_read:
    in ax,dx
    mov [ebx],ax
    add ebx,2
    loop .go_on_read
    ret

setup_page:
    mov ecx,4096
    mov esi,0
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS+esi],0
    inc esi
    loop .clear_page_dir
    ;magic_break
    ;xchg bx,bx
;开始创建页目录项(PDE)
.create_pde:
    mov eax,PAGE_DIR_TABLE_POS ;page dir addr
    add eax,0x1000             ;first page table addr
    mov ebx,eax
    or eax,PG_US_U | PG_RW_W | PG_P  ;set page dir attrs
    mov [PAGE_DIR_TABLE_POS + 0x0],eax ;set first page dir element attrs
    mov [PAGE_DIR_TABLE_POS + 0xc00],eax ;0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间
    sub eax,0x1000
    mov [PAGE_DIR_TABLE_POS + 4092],eax ;使最后一个目录项指向页目录表自己的地址
;创建页表项(PTE)
    mov ecx,256 ; 1M低端内存 / 每页大小4k = 256
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P
    ;magic_break
    ;xchg bx,bx
.create_pte:
    mov [ebx+esi*4],edx
    add edx,4096
    inc esi
    loop .create_pte
;创建内核其它页表的PDE
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000
    or eax, PG_US_U | PG_RW_W | PG_P
    mov ebx, PAGE_DIR_TABLE_POS
    mov ecx, 254 ; 范围为第769~1022的所有目录项数量
    mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

section .data
gdt_base:
    ;zeroth sector descriptor is zero and can't be indexed
    dd 0x00000000
    dd 0x00000000
    ;first sector descriptor
desc_code:
    dd  0x0000ffff
    dd  DESC_CODE_HIGH4
    ;second sector descriptor
desc_data:
    dd  0x0000ffff
    dd  DESC_DATA_HIGH4
    ;third sector descriptor
desc_video:
    dd  0x80000007
    dd  DESC_VIDEO_HIGH4

GDT_SIZE equ ($-gdt_base)
GDT_LIMIT equ (GDT_SIZE-1)
times 60 dq 0
SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL_0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL_0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL_0

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记
total_mem_bytes dd 0

gdt_ptr:
    dw GDT_LIMIT
    dd gdt_base
;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0  ;用于记录ards结构体数量